package com.linkoog.devtools.http.client;

import com.linkoog.devtools.common.BizException;
import com.linkoog.devtools.http.common.HttpMethod;
import com.linkoog.devtools.http.request.common.HttpRequest;
import com.linkoog.devtools.http.request.common.HttpResponse;
import com.linkoog.devtools.http.request.common.RequestBody;
import com.linkoog.devtools.http.request.common.ResponseBody;
import com.linkoog.devtools.http.request.common.request_body.FileRequestBody;
import com.linkoog.devtools.http.request.common.request_body.MultipartFormRequestBody;
import com.linkoog.devtools.http.request.common.request_body.NoneRequestBody;
import com.linkoog.devtools.http.request.common.request_body.UrlEncodeFormRequestBody;
import com.linkoog.devtools.http.request.common.request_body.text.JsonRequestBody;
import com.linkoog.devtools.http.request.common.request_body.text.PlainTextRequestBody;
import com.linkoog.devtools.http.request.common.request_body.text.XmlRequestBody;
import com.linkoog.devtools.http.request.common.response_body.NoneResponseBody;
import com.linkoog.devtools.http.request.common.response_body.text.HtmlResponseBody;
import com.linkoog.devtools.http.request.common.response_body.text.JsonResponseBody;
import com.linkoog.devtools.http.request.common.response_body.text.PlainTextResponseBody;
import com.linkoog.devtools.http.request.common.response_body.text.XmlResponseBody;
import com.linkoog.devtools.utils.StringUtils;
import okhttp3.FormBody;
import okhttp3.Headers;
import okhttp3.MediaType;

import java.io.File;
import java.net.FileNameMap;
import java.net.URLConnection;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.security.SecureRandom;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;

import okhttp3.MultipartBody;
import okhttp3.OkHttpClient;
import okhttp3.Request;

import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;

public class HttpClient {

    private final static MediaType textType = MediaType.parse("text/plain");
    private final static MediaType xmlType = MediaType.parse("application/xml; charset=utf-8");
    private final static MediaType jsonType = MediaType.parse("application/json; charset=utf-8");


    private final static MediaType formdataType = MediaType.parse("multipart/form-data");
    private final static MediaType urlencodedType = MediaType.parse("application/x-www-form-urlencoded; charset=utf-8");
    private final static MediaType octetStream = MediaType.parse("application/octet-stream");


    private boolean isFile(String contentType){
        return contentType.startsWith("application/octet-stream");
    }

    private boolean isXlsFile(String contentType){
        return contentType.startsWith("application/vnd.ms-excel");
    }

    private boolean isXlsxFile(String contentType){
        return contentType.startsWith("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
    }


    public HttpResponse send(HttpRequest httpRequest) throws BizException{
        long start = System.currentTimeMillis();


        try {
            Request.Builder requestBuilder = new Request.Builder();
            HttpMethod httpMethod = httpRequest.getHttpMethod();
            if (HttpMethod.GET == httpMethod) {
                requestBuilder.get();
            } else if (HttpMethod.POST == httpMethod) {
                okhttp3.RequestBody requestBody = buildRequestBody(httpRequest);
                requestBuilder.post(requestBody);
            } else if (HttpMethod.PUT == httpMethod) {
                okhttp3.RequestBody requestBody = buildRequestBody(httpRequest);
                requestBuilder.put(requestBody);
            } else if (HttpMethod.DELETE == httpMethod) {
                okhttp3.RequestBody requestBody = buildRequestBody(httpRequest);
                requestBuilder.delete(requestBody);
            } else if (HttpMethod.HEAD == httpMethod) {
                requestBuilder.head();
            } else if (HttpMethod.OPTIONS == httpMethod) {
                okhttp3.RequestBody requestBody = buildRequestBody(httpRequest);
                requestBuilder.method("OPTIONS", requestBody);
            } else if (HttpMethod.PATCH == httpMethod) {
                okhttp3.RequestBody requestBody = buildRequestBody(httpRequest);
                requestBuilder.method("PATCH", requestBody);
            }

            Map<String, List<String>> headerData = httpRequest.getHeaders();
            if (headerData != null && !headerData.isEmpty()){
                for (String headerName : headerData.keySet()) {
                    if (StringUtils.isBlank(headerName)) continue;

                    List<String> headerValues = headerData.get(headerName);
                    if (headerValues == null || headerValues.isEmpty()) continue;

                    for (String headerValue : headerValues) {
                        if (StringUtils.isBlank(headerValue)) continue;
                        requestBuilder.addHeader(encodedHeaderValue(headerName), encodedHeaderValue(headerValue));
                    }
                }
            }

            String url = buildRequestUrl(httpRequest.getUrl(), httpRequest.getParams());
            requestBuilder.url(url);

            Request request = requestBuilder.build();
            OkHttpClient okHttpClient = createOkHttpClient();

            okhttp3.Response response = okHttpClient.newCall(request).execute();

            int status = response.code();
            long costTime = System.currentTimeMillis() - start;
            Map<String, List<String>> responseHeaders = new LinkedHashMap<>();
            Headers headers = response.headers();
            for (String name : headers.names()) {
                List<String> values = headers.values(name);
                responseHeaders.put(name, values);
            }

            ResponseBody responseBody = null;
            okhttp3.ResponseBody body = response.body();
            if (body == null) {
                responseBody = NoneResponseBody.INSTANCE;
            } else {
                String contentType = response.header("content-type");
                if (StringUtils.isNotBlank(contentType)) {
                    if (contentType.contains("application/json")) {
                        responseBody = JsonResponseBody.of(body.string());
                    } else if (contentType.contains("application/xml")) {
                        responseBody = XmlResponseBody.of(body.string());
                    } else if (contentType.contains("text/html")) {
                        responseBody = HtmlResponseBody.of(body.string());
                    } else if (contentType.contains("text/plain")) {
                        responseBody = PlainTextResponseBody.of(body.string());
                    }
                } else {
                    responseBody = PlainTextResponseBody.of(body.string());
                }
            }

            return HttpResponse.builder()
                    .status(String.valueOf(status))
                    .costTime(String.valueOf(costTime))
                    .headers(responseHeaders)
                    .size(String.valueOf(response.body().contentLength()))
                    .responseBody(responseBody)
                    .build();
        } catch (Exception e){
            throw new BizException("create OkHttpClient error: " + e.getMessage());
        }
    }

    private OkHttpClient createOkHttpClient(){
        try {
            FakeX509TrustManager myTrustManager = new FakeX509TrustManager();
            FakeHostnameVerifier myHostnameVerifier = new FakeHostnameVerifier();
            SSLContext sslCtx = SSLContext.getInstance("TLS");
            sslCtx.init(null, new TrustManager[]{ myTrustManager }, new SecureRandom());
            SSLSocketFactory mySSLSocketFactory = sslCtx.getSocketFactory();
            OkHttpClient client = new OkHttpClient.Builder()
                    .followRedirects(false)
                    .followSslRedirects(false)
                    .connectTimeout(15, TimeUnit.SECONDS)
                    .writeTimeout(15, TimeUnit.SECONDS)
                    .readTimeout(20, TimeUnit.SECONDS)
                    //.addNetworkInterceptor(logInterceptor)
                    .sslSocketFactory(mySSLSocketFactory, myTrustManager)
                    .hostnameVerifier(myHostnameVerifier)
                    //.dns(new LocalDns(local))
                    .build();
            return client;
        }catch (Exception e){
            throw new BizException("create OkHttpClient error: " + e.getMessage());
        }
    }

    private String buildRequestUrl(String httpRequestUrl, Map<String, List<String>> queryParams) throws BizException {
        if (httpRequestUrl == null || !(httpRequestUrl.startsWith("http://") || httpRequestUrl.startsWith("https://"))) {
            throw new BizException("url must start with http:// or https://");
        }

        if (queryParams != null && !queryParams.isEmpty()) {
            StringBuilder queryString = new StringBuilder();
            for (String name : queryParams.keySet()) {
                if (name == null || name.isEmpty()){
                    continue;
                }

                List<String> values = queryParams.get(name);
                if (values == null || values.isEmpty()){
                    continue;
                }

                for (String value : values) {
                    if (StringUtils.isBlank(value)){
                        value = "";
                    }

                    //替换路径参数
                    httpRequestUrl = httpRequestUrl.replaceAll("\\{" + name + "}", value);
                    queryString.append("&").append(name).append("=").append(URLEncoder.encode(value, StandardCharsets.UTF_8));
                }
            }

            if (! queryString.isEmpty()){
                String query = queryString.substring(1);
                if (! httpRequestUrl.contains("?")){
                    httpRequestUrl = httpRequestUrl + "?" + query;
                } else {
                    if (httpRequestUrl.endsWith("?") || httpRequestUrl.endsWith("&")){
                        httpRequestUrl = httpRequestUrl + query;
                    } else {
                        httpRequestUrl = httpRequestUrl + "&" + query;
                    }
                }
            }
        }

        return httpRequestUrl;
    }



    private okhttp3.RequestBody buildRequestBody(HttpRequest httpRequest) throws BizException{
        RequestBody requestBody = httpRequest.getBody();
        if (requestBody instanceof NoneRequestBody){
            return okhttp3.RequestBody.create("", jsonType);
        } else if (requestBody instanceof PlainTextRequestBody body){
            return okhttp3.RequestBody.create(body.getValue(), textType);
        } else if (requestBody instanceof XmlRequestBody body){
            return okhttp3.RequestBody.create(body.getValue(), xmlType);
        } else if (requestBody instanceof JsonRequestBody body){
            return okhttp3.RequestBody.create(body.getValue(), jsonType);
        } else if (requestBody instanceof UrlEncodeFormRequestBody body){
            FormBody.Builder builder = new FormBody.Builder();
            Map<String, List<String>> bodyData = body.getDataMap();
            for (String key : bodyData.keySet()) {
                List<String> values = bodyData.get(key);
                if (values == null || values.isEmpty()){
                    continue;
                }
                for (String value : values) {
                    if (StringUtils.isBlank(value)){
                        value = "";
                    }
                    builder.addEncoded(key, value);
                }
            }
            return builder.build();
        } else if (requestBody instanceof FileRequestBody body){
            if (StringUtils.isBlank(body.getFilePath())){
                throw new BizException("filePath is blank");
            }
            MultipartBody.Builder builder = new MultipartBody.Builder().setType(MultipartBody.FORM);
            File file = new File(body.getFilePath());
            if (! file.exists()){
                throw new BizException("filePath not exists: " + body.getFilePath());
            } else if (! file.isFile()){
                throw new BizException("filePath is directory: " + body.getFilePath());
            }
            String fileType = getMimeType(file.getName());
            if (StringUtils.isBlank(body.getParamName())){
                builder.addPart(okhttp3.RequestBody.create(file, MediaType.parse(fileType)));
            } else {
                builder.addFormDataPart(body.getParamName(), file.getName(), okhttp3.RequestBody.create(file, MediaType.parse(fileType)));
            }
            return builder.build();
        } else if (requestBody instanceof MultipartFormRequestBody body){
            MultipartBody.Builder builder = new MultipartBody.Builder().setType(MultipartBody.FORM);
            for (MultipartFormRequestBody.MultipartFormItem item : body.getDataList()) {
                if (StringUtils.isBlank(item.getName())){
                    throw new BizException("name is blank: " + item);
                }

                if (item.isFile()){
                    File file = new File(item.getValue());
                    if (! file.exists()){
                        throw new BizException("filePath not exists: " + item.getValue());
                    } else if (! file.isFile()){
                        throw new BizException("filePath is directory: " + item.getValue());
                    }

                    String fileType = getMimeType(file.getName());
                    builder.addFormDataPart(item.getName(), file.getName(), okhttp3.RequestBody.create(file, MediaType.parse(fileType)));
                } else {
                    builder.addFormDataPart(item.getName(), item.getValue());
                }
            }
            return builder.build();
        }
        return okhttp3.RequestBody.create("", jsonType);
    }

    /**
     * 获取文件MimeType
     */
    private static String getMimeType(String fileName) {
        FileNameMap filenameMap = URLConnection.getFileNameMap();
        String contentType = filenameMap.getContentTypeFor(fileName);
        if (contentType == null) {
            contentType = "application/octet-stream";
        }
        return contentType;
    }

    //由于okhttp header 中的 value 不支持 null, \n 和 中文这样的特殊字符,所以这里
    //会首先替换 \n ,然后使用 okhttp 的校验方式,校验不通过的话,就返回 encode 后的字符串
    private static String encodedHeaderValue(String value) {
        if (value == null) return "null";
        String newValue = value.replace("\n", "");
        for (int i = 0, length = newValue.length(); i < length; i++) {
            char c = newValue.charAt(i);
            if (c <= '\u001f' || c >= '\u007f') {
                return URLEncoder.encode(newValue, StandardCharsets.UTF_8);
            }
        }
        return newValue;
    }


}
